package com.onpositive.utils;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

import org.eclipse.core.runtime.Platform;

public class NumericValueValidityProvider extends AbstractValidityProvider<Double>
{

	private static final double epsilon = 0.0000001 ;
	private final static double criticalRatio = 0.05 ;	
	private final static double baseRatio = 0.01 ;
	
	public static double getEpsilon() {
		return epsilon;
	}

	private int lowerValidIndex, upperValidIndex ;
	private boolean isPositive ;
	ArrayList<Double> elements ;
	private double[] epsilonsArray ;
	
	public NumericValueValidityProvider(Iterable<Double> array, String comment)
	{
		
		super(array);
		elements = new ArrayList<Double>() ;
		
		for( Number n : array )
			if( n != null )
				elements.add( n.doubleValue() ) ;
				
		init(elements,comment);						
	}

	private void init( List<Double> elements, String comment )
	{
		int N = elements.size() ;
				
		if( N < 2 )
		{
			setCorrect(false) ;
			return ;
		}
		
		Decomposition decomposition = new Decomposition( elements, this ) ;
		if( !isCorrect() )
			return ;
		
//		lowerValidBound = decomposition.minValidIntervalStart ;
//		upperValidBound = decomposition.maxValidIntervalEnd ;
//		isPositive = lowerValidBound > (-1)*epsilon ;
//		
//		double var = decomposition.intervalLength /(1 << decomposition.minGapDepth/2 );
//		
//		if( !isPositive )
//			lowerValidBound -= var ;
//		else
//			lowerValidBound = epsilon ;
//		
//		upperValidBound += var ;
		
		final Interval[] intervalsInfo = decomposition.getLeafIntervals() ;
		int size = intervalsInfo.length ;
		
		if( size == 0 )
			return ;
		
		Integer[] index = new Integer[size] ;
		for( int i = 0 ; i < size ; i++ )
			index[i] = i ;
		
		Arrays.sort( index, new Comparator<Integer>(){
			@Override
			public int compare(Integer o1, Integer o2){
				double dif = intervalsInfo[o1].start - intervalsInfo[o2].start;
				if( Math.abs(dif) < epsilon )
					return 0 ;
				
				return dif > 0 ? 1 : -1;
			}			
		}) ;
		
		double mult = 1.0/40 ;
		this.epsilonsArray = new double[size<<1] ;
		
		
		epsilonsArray[0] = intervalsInfo[ index[0] ].start ;
		double l_ = 2*intervalsInfo[ index[0] ].length;
		epsilonsArray[1] = l_*mult ;
		
		for( int i = 1 ; i < size ; i++ ){ 
			epsilonsArray[2*i] = intervalsInfo[ index[i] ].start ;
			double l0 = intervalsInfo[ index[i-1] ].length;
			double l1 = intervalsInfo[ index[i] ].length;
			epsilonsArray[2*i+1] = (l0+l1)*mult ;
		}		
		
		
		int[] stats_pc = new int[ decomposition.pointsPerInterval+1 ] ;
		int[] stats_d = new int[ decomposition.maxDepth+1 ] ;
		
		double[] stats = processStats(intervalsInfo, size, index, stats_pc, stats_d);
		
		double avarageDepth = stats[2] ;
		if( avarageDepth < 4.1 ){
			lowerValidIndex = 0 ;
			upperValidIndex = N-1 ;
			setCorrect(false) ;
			return ;
		}
		
		int ii0 = index[0];
		Interval intreval_0 = intervalsInfo[ ii0 ];
		int iDepth = intreval_0.depth;
		int iSize = intreval_0.endIndex - intreval_0.startIndex;
		
		if( iDepth == 1 && iSize == 1 )			
			lowerValidIndex =  1 ;
		else
			lowerValidIndex = 0 ;
		
		
		int ii1 = index[size-1];
		Interval interval_N = intervalsInfo[ ii1 ];
		iDepth = interval_N.depth;
		iSize = interval_N.endIndex - interval_N.startIndex;
		
		if( iDepth == 1 && iSize == 1 )			
			upperValidIndex = N-2 ;
		else
			upperValidIndex = N-1 ;
		
		//saveLog(decomposition, elements, intervalsInfo, size, index, stats_pc, stats_d, stats, comment);
	}

	public static void clearLog(){
		File rootDir = new File( Platform.getInstanceLocation().getURL().getPath() ) ;
		File logDir = new File( rootDir, "logs/" ) ;
		logDir.mkdirs() ;
		File logFile = new File( logDir, "stats-log.txt" ) ;
		
		try {
			if( !logFile.exists() )
				logFile.createNewFile() ;
			
			PrintStream ps = new PrintStream( logFile ) ;
			ps.print("") ;
			ps.close() ;
		}
		catch( Exception e ){
			
		}
	}
	private void saveLog(
			Decomposition decomposition,
			List<Double> elements,
			final Interval[] intervalsInfo, int size, Integer[] index,
			int[] stats_pc, int[] stats_d, double[] stats, String comment )
	{
		File rootDir = new File( Platform.getInstanceLocation().getURL().getPath() ) ;
		File logDir = new File( rootDir, "logs/" ) ;
		logDir.mkdirs() ;
		File logFile = new File( logDir, "stats-log.txt" ) ;
		try {
			if( !logFile.exists() )
				logFile.createNewFile() ;
			
			FileOutputStream fos = new FileOutputStream( logFile, true ) ;  
			PrintStream ps = new PrintStream( fos ) ;
			
			double average_pc = stats[0] ;
			double dispersion_pc = stats[1] ;
			double average_d = stats[2] ;
			double dispersion_d = stats[3] ;
		
			ps.println( comment );
			ps.println();
			ps.print( "average point count = " );
			ps.print( average_pc) ;
			ps.print( "; point count dispersion = " );
			ps.println(dispersion_pc) ;
			ps.print( "average depth = " );
			ps.print( average_d) ;
			ps.print( "; depth dispersion = " );
			ps.println(dispersion_d) ;
			ps.println();
			{
				int b0 = Math.min( size, 5) ;
				int b1 = Math.max( size-6, 0) ;
				boolean bl = true ;
				for( int i = 0 ; i < size ; i++ )
				{
					if( !(i < b0 || i > b1 ) ){						
						if(bl){
							ps.println( "- - - - -" ) ;
							bl = false ;
						}
						continue ;					
					}
						
					Interval interval = intervalsInfo[ index[i] ];
					int pc = interval.actualPointsCount;
					int d = interval.depth;
						
					ps.print( "st: " ) ;			
					ps.print( interval.start ) ;
					ps.print( "; lg: " ) ;
					ps.print( interval.length ) ;
					ps.print( "; pc: " ) ;			
					ps.print( pc ) ;
					ps.print( "; d: " ) ;			
					ps.print( d ) ;
					ps.println();			
				}
			}
			ps.println();			
	
			{
				int sz = elements.size();
				int b0 = Math.min( sz, 10) ;
				int b1 = Math.max( sz-11, 0) ;
				boolean bl = true ;
				for( int i = 0 ; i < sz ; i++ )
				{
					if( !(i < b0 || i > b1 ) ){						
						if(bl){
							ps.println( "- - - - -" ) ;
							bl = false ;
						}
						continue ;					
					}						
					ps.println( elements.get(i) );			
				}
			}
			ps.println();			
				
			ps.println( "point count stats" );
			for( int i = 0 ; i < stats_pc.length ; i++ ){
				ps.print(i) ;
				ps.print(":") ;
				ps.print(stats_pc[i]) ;
				ps.print("; ") ;	
			}		
			ps.println();		
				
			ps.println( "depth stats" );
			for( int i = 0 ; i < stats_d.length ; i++ ){
				ps.print(i) ;
				ps.print(":") ;
				ps.print(stats_d[i]) ;
				ps.print("; ") ;
			}				
			ps.println();
			ps.println();
			ps.close() ;
		
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
	}

	private double[] processStats(final Interval[] intervalsInfo, int size,
			Integer[] index, int[] stats_pc, int[] stats_d) {
		double[] stats;
		{	
		
		
		for( int i = 0 ; i < size ; i++ )
		{
			Interval interval = intervalsInfo[ index[i] ];
			int pc = interval.actualPointsCount;
			int d = interval.depth;
			stats_pc[pc]++ ;
			stats_d[d]++ ;
		}
		
		double average_pc = 0 ;
		int count = 0 ;
		boolean started = false ;
		for( int i = 0 ; i < stats_pc.length ; i++ )
		{
			if( stats_pc[i] == 0 )
				continue ;
			
			started = true ;
			
			average_pc += i*stats_pc[i] ;
			count += stats_pc[i] ;
		}
		average_pc /= count ;
		
		started = false ;
		double dispersion_pc = 0 ;
		for( int i = 0 ; i < stats_pc.length ; i++ )
		{
			if( stats_pc[i] == 0 )
				continue ;
			
			started = true ;
			
			dispersion_pc += i*i*stats_pc[i] ;
		}
		dispersion_pc /= count ;
		dispersion_pc -= average_pc*average_pc ;
		
		count = 0 ;
		double average_d = 0 ;
		started = false ;
		for( int i = 0 ; i < stats_d.length ; i++ )
		{
			if( stats_d[i] == 0 )
				continue ;
			
			started = true ;
			
			average_d += i*stats_d[i] ;
			count += stats_d[i] ;
		}
		average_d /= count ;
		
		started = false ;
		double dispersion_d = 0 ;
		for( int i = 0 ; i < stats_d.length ; i++ )
		{
			if( stats_d[i] == 0 )
				continue ;
			
			started = true ;
			
			dispersion_d += i*i*stats_d[i] ;
		}
		dispersion_d /= count ;
		dispersion_d -= average_d*average_d ;
		
		stats = new double[]{
		average_pc,
		dispersion_pc,
		average_d,
		dispersion_d } ;
		}
		return stats;
	}
	
	public double[] getEpsilonArray()
	{		
		return epsilonsArray ;
	}

	@Override
	public Boolean getValidity(Double var) {
		
		Double d = var.doubleValue() ;
		return d > lowerValidIndex - epsilon && d < upperValidIndex + epsilon ;
	}
	
	private static enum bounds{
		MIN,
		MAX
	}
	private class Decomposition{
		
		double lowerBound, upperBound, intervalLength, maxValidIntervalEnd, minValidIntervalStart ;
		int maxDepth, minGapDepth, pointsPerInterval ;
		ArrayList<Interval> subintervals ; 
		
		
		Decomposition( List<Double> elements, NumericValueValidityProvider parent )
		{
			if( elements == null || elements.size() == 0 )
				return ;		
			
			int N = elements.size() ;			
			
			Double rawLowerBound = computeActualBound( elements, bounds.MIN ) ;
			Double rawUpperBound = computeActualBound( elements, bounds.MAX ) ;
			if( rawLowerBound == null || rawUpperBound == null ){
				parent.setCorrect( false ) ;
				return ;
			}
			
			lowerBound = rawLowerBound ;
			upperBound = rawUpperBound ;
			maxValidIntervalEnd = lowerBound ;
			minValidIntervalStart = upperBound ;
			intervalLength = upperBound - lowerBound ;
			
			subdivide(elements, N);
			
			minGapDepth = maxDepth ;
			int s = subintervals.size() ;
			for( int i = 1 ; i < s ; i++ )			
			{
				Interval il = subintervals.get(i) ;
				
				if( il.child > 0 )
					continue ;
				
				if( il.start > minValidIntervalStart + epsilon && il.start + il.length < maxValidIntervalEnd - epsilon )
				{
					minGapDepth = minGapDepth > il.depth ? il.depth : minGapDepth ;
					break ;
				}
			}			
		}
		
		private void subdivide(List<Double> elements, int N )
		{
			Collections.sort( elements ) ;
			int size = elements.size();
			double[] testArr = new double[ size ] ;
			for( int i = 0 ; i < size ; i++ )
			{
				Double dbl = elements.get(i) ;
				testArr[i] = dbl != null ? dbl : Double.NaN ;
			}
			
			pointsPerInterval = (int)(baseRatio * N + 1) ;
			
			subintervals = new ArrayList<NumericValueValidityProvider.Interval>() ;
			subintervals.add( new Interval(0, N, 0, lowerBound, intervalLength, elements)) ;
		
			maxDepth = 0 ;
			ArrayList<Double> child0 = new ArrayList<Double>() ;
			ArrayList<Double> child1 = new ArrayList<Double>() ;
			
			for( int i = 0 ; i < subintervals.size() ; i++ ){
				Interval il = subintervals.get(i) ;
				double s = il.start;
				double l = il.length;
				
				if( il.actualPointsCount <= pointsPerInterval || il.allSame )
				{
					if( il.actualPointsCount == 0 ){
						il.child = -10 ;
						continue ;
					}
					il.child = -1 ;
					
					minValidIntervalStart = s < minValidIntervalStart ? s : minValidIntervalStart ;
					maxValidIntervalEnd = s + l > maxValidIntervalEnd ? s + l : maxValidIntervalEnd ;
					
					maxDepth = Math.max( maxDepth, il.depth ) ;
					continue ;
				}
				processInterval(elements, i, child0, child1);
			}
			
			
			
//			int midSize = subintervals.size() ;
//			double dAvarageDepth = 0 ;
//			int count = 0 ;
//			for( Interval il : subintervals )
//			{								
//				if( il.child != -1  )
//					continue ;
//				
//				dAvarageDepth += il.depth ;
//				count++ ;
//			}
//			
//			int avarageDepth = (int)( dAvarageDepth/count + 0.5 ) ;			
//			
//			for( int i = 0 ; i < midSize ; i++ )
//			{				
//				Interval il = subintervals.get(i) ;
//				if( il.child != -1 || il.depth > avarageDepth || il.actualPointsCount == 0 ){
//					continue ;
//				}
//				
//				processInterval(elements, i, child0, child1);
//			}
//			
//			for( int i = midSize ; i < subintervals.size() ; i++ )
//			{				
//				Interval il = subintervals.get(i) ;
//				if( il.depth > avarageDepth || il.actualPointsCount == 0 ){
//					il.child = -1 ;
//					continue ;
//				}
//				processInterval( elements, i, child0, child1 );
//			}
			
			
		}

		private void processInterval(List<Double> elements, int i, ArrayList<Double> child0, ArrayList<Double> child1)
		{			
			Interval il = subintervals.get(i) ;
			double s = il.start;
			double l = il.length;			

			child0.clear() ;
			child1.clear() ;
			
			double barrier = s + l*0.5 ;
			
			for( int j = il.startIndex ; j < il.endIndex ; j++ )
			{
				double d = elements.get(j) ;
				if( d > barrier || Math.abs( d-barrier ) < epsilon )
					child1.add(d) ;
				else
					child0.add(d) ;					
			}
			int j = il.startIndex-1 ;

			for( Double d : child0 )
				elements.set( ++j, d ) ;
					
			for( Double d : child1 )
				elements.set( ++j, d ) ;		
				
			Interval cInterval0 = new Interval(il.startIndex, il.startIndex+child0.size(), il.depth+1, s, l*0.5, elements ) ;
			Interval cInterval1 = new Interval(il.startIndex+child0.size(), il.endIndex, il.depth+1, s +l*0.5, l*0.5, elements ) ;
			
			il.child = subintervals.size() ;
			subintervals.add( cInterval0 ) ;
			subintervals.add( cInterval1 ) ;
			
//			double ratio0 = (double)child0.size()/child1.size() ;
//			if( ratio0 < criticalRatio )
//				cInterval0.child = -1 ;
//			else
//				minValidIntervalStart = cInterval0.start < minValidIntervalStart ? cInterval0.start : minValidIntervalStart ;
//			
//			double ratio1 = (double)child1.size()/child0.size() ;
//			if( ratio1 < criticalRatio )
//				cInterval1.child = -1 ;
//			else
//				maxValidIntervalEnd = cInterval1.start + cInterval1.length > maxValidIntervalEnd ? cInterval1.start + cInterval1.length : maxValidIntervalEnd ;
//			
//			maxDepth = maxDepth < il.depth + 1 ? il.depth + 1 : maxDepth ;					

		}
		
		private Interval[] getLeafIntervals()
		{			
			ArrayList<Interval> lst = new ArrayList<Interval>() ;
			for( Interval il : this.subintervals ){
				if( il.child == -1 )				
					lst.add( il ) ;
			}
			
			int size = lst.size();
			Interval[] result = new Interval[ size ] ;
			for( int i = 0 ; i < size ; i++ )
				result[i] = lst.get(i) ;
			
			return result ;
		}
	}
	
	private class Interval{
		
		public Interval(int startIndex, int endIndex, int depth, double start, double length, List<Double> elements)
		{
			super();
			this.startIndex = startIndex;
			this.endIndex = endIndex;
			this.depth = depth;
			this.start = start;
			this.length = length;
			this.allSame = true;
			this.child = 0 ;
			this.actualPointsCount = 0 ;
			
			if( endIndex - startIndex < 1 )
				return ;
			
			List<Double> lst = elements.subList(startIndex, endIndex) ;
			Collections.sort(lst) ;
			
			Double prev = lst.get(0) ;
			int count = 1 ;
			for( Double d : lst )
			{
				if( Math.abs( d - prev ) > epsilon ){
					prev = d ;
					count++ ;
				}				
			}
			
			this.allSame = count == 1 ;
			this.actualPointsCount = count ;
		}
		int startIndex, endIndex, depth, actualPointsCount ;
		double start, length ;
		int child ;//actually, we indicate only the left Child. The right child has index this.child+1 ;
		boolean allSame ;
	}

	public ArrayList<Double> getElements() {
		return elements;
	}

	

	public int getLowerValidIndex() {
		return lowerValidIndex;
	}

	public int getUpperValidIndex() {
		return upperValidIndex;
	}

	public boolean isPositive() {
		return isPositive;
	}
	
	private Double computeActualBound( List<Double> elements, bounds b )
	{
		if( elements == null || elements.size() == 0 )
			return null ;
		
		if( b.equals(bounds.MAX) )
		{
			Double result = Double.NEGATIVE_INFINITY ;				
			for( Double d : elements )
				if( d > result && d != Double.POSITIVE_INFINITY )
					result = d ;
			
			return result != Double.NEGATIVE_INFINITY ? result : null ;
		}
		if( b.equals(bounds.MIN) )
		{
			Double result = Double.POSITIVE_INFINITY ;				
			for( Double d : elements )
				if( d < result && d != Double.NEGATIVE_INFINITY )
					result = d ;
			
			return result != Double.POSITIVE_INFINITY ? result : null ;
		}
		return null ;			
	}

	private boolean isCorrect = true ;
	
	public void setCorrect(boolean b) {
		isCorrect = b ;		
	}
	
	public boolean isCorrect() {		
		return isCorrect;
	}

}
